feat: preserve Leica confocal channel identity (dye, excitation, emission) in OME-Zarr#32
Conversation
…tation/emission/detector) Pure stdlib, hardened (DTD/entity-bomb fail-closed), bioio-free so it can lift into bioio-lif.ome_metadata. Resolves channels via LDM_Block_Sequential_List, dodging the phantom 620nm + duplicate settings blocks. Slice 1 of the channel-metadata run.
…mero labels For Leica LIF, the per-scene path now builds a real OME Image (dye name, excitation & emission wavelengths) and omero labels like 'ALEXA 594 (590 nm)' instead of a MetadataOnly stub. LIF-only (keyed on scene_root), fail-safe (any error falls back to the prior name-based path), channel-count vs SizeC guarded, CZI/ND2 untouched. Slice 2 of the channel-metadata run.
Note: collapse the in-tree extractor onto
|
…e scenes
_lif_scene_channels located each scene's settings XML via reader.scene_root,
bioio-lif's plate row/column locator, which raises ValueError for ordinary
non-plate confocal scenes — so the confocal channel-metadata feature was
silently inert (output fell back to display-LUT labels like Blue/Gray and a
MetadataOnly OME-XML stub).
Locate the current scene via reader.metadata.findall(".//Image")[current_scene_index]
(aligned with bioio-lif PR ferrinm#52), keeping reader.scene_root as a plate fast-path.
Every fail-safe is preserved: the channel-count-vs-SizeC guard (both directions)
and a clean (None, None) decline on any unexpected reader-surface error, so
metadata never crashes a conversion.
Tests: confocal-path regression cases folded into test_api_lif_wiring (real
captured fixture, no bioio_lif import / no large file, CI-safe), with the seam's
docstring updated for the new two-tier locator. CHANGELOG: Unreleased entry for
the confocal channel-identity feature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9a620ab to
8bd694a
Compare
Resolves the CHANGELOG [Unreleased] conflict by keeping both the confocal channel-identity entry and main's size/OME-NGFF-validation entries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Problem
zarrmony's promise is "convert any bioimage to OME-Zarr, preserving metadata." For
Leica confocal
.liffiles that promise was silently broken: every channel'sfluorophore, excitation laser line, emission detection band, and detector were dropped.
Root cause is two gaps in
bioio-lif, now filed + fixed upstream(bioio-devs/bioio-lif#51, PR bioio-devs/bioio-lif#52):
ChannelDescription/@LUTName— a display LUTcolor (
"Blue","Gray","Green", …), not the fluorophore.ome_metadata(inheritsbioio_base'sNotImplementedError), soapi._try_get_ome_imagefell back to a<MetadataOnly/>stub — no OME-XML channelmetadata at all. (CZI/ND2 plugins do implement it; LIF was the odd one out.)
The real identity is all present in the LIF XML, which we already preserve verbatim at
OME/source/raw.lif.xml. This PR recovers it.What changed
src/zarrmony/metadata/lif_channels.py(new) — a pure, stdlib-only,dependency-free extractor:
extract_channels(scene_xml)→ per-channel{index, dye, fluor, detector, excitation_nm, emission_low_nm, emission_high_nm}. The join is the careful part: thereal acquisition sequences are the
ATLConfocalSettingDefinitionchildren ofLDM_Block_Sequential/LDM_Block_Sequential_List, indexed by each channel'sSequentialSettingIndex— which correctly ignores the duplicate…Masterblock anda phantom 620 nm setting that would otherwise mislabel channels. Excitation is the
active laser line paired to the active detector by spectral order; emission is the
detector's
MultiBandwindow.channels_to_ome_channels(...)/channels_to_omero(...)— project that identity intoOME-XML
<Channel>(Name/Fluor/ExcitationWavelength/EmissionWavelength) and omeroDye (NNN nm)display labels + colors.fails closed (returns
[]/ falls back) on any malformed input — never raises,never hangs.
src/zarrmony/api.py— for LIF the per-scene path now builds a real OMEImage(with channels) and meaningful omero labels instead of the stub. LIF-only (keyed on
reader.scene_root), fail-safe (any extraction error reverts to the priorname-based path — metadata never breaks a conversion), and channel-count vs
SizeCguarded so it can't mislabel. CZI/ND2/non-LIF paths are untouched.
tests/fixtures/lif_confocal_7ch.xml,no multi-GB file needed) + extractor, projection, and api-wiring (fail-safe) suites,
including the entity-bomb / non-plate-
scene_root-raises / count-mismatch edges.The same extraction has been ported upstream into
bioio-lif's ownome_metadata(Confocal channel identity: implement
ome_metadataand name channels by fluorophore bioio-devs/bioio-lif#52); once that ships, this LIF-specific layer can be retired infavor of it. Until then, zarrmony carries it so conversions preserve the metadata today.
Verification
On a real 7-channel Leica STELLARIS confocal scene (
CohortAI37), the converted store nowcarries, in order:
DAPI (405 nm),ALEXA 594 (590 nm),ALEXA 750 (753 nm),ALEXA 488 (499 nm),ALEXA 647-R-PE (653 nm),ALEXA 555 (553 nm),ALEXA 700 (696 nm)— matching the dyes/lasers/emission bands cross-checked against the acquisition metadata.
uv run ruff check/pytest/black --check ./npx prettier --check .all green.Compatibility / scope
Additive and back-compatible: no change to CZI/ND2/non-LIF behavior, the audit schema, or
existing outputs. Out of scope (deferred): emission-derived omero colors; the bf2raw/plate
LIF paths (per-scene is the default LIF layout); widefield channel enrichment.